D:\a\scloud-dns\scloud-dns\src\config.rs
Line | Count | Source |
1 | | //! Configuration types for scloud-dns |
2 | | //! |
3 | | //! This file contains Serde (Deserialize/Serialize) structs that map to the |
4 | | //! JSON configuration you provided. It includes helpers to load the config |
5 | | //! from a file and a light `validate()` method placeholder you can extend. |
6 | | |
7 | | use crate::exceptions::SCloudException; |
8 | | use anyhow::{Context, Result}; |
9 | | use serde::{Deserialize, Serialize}; |
10 | | use std::collections::HashSet; |
11 | | use std::fs; |
12 | | use std::path::Path; |
13 | | |
14 | | /// Top-level configuration |
15 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
16 | | pub struct Config { |
17 | | #[serde(default)] |
18 | | pub server: ServerConfig, |
19 | | |
20 | | #[serde(default)] |
21 | | pub workers: WorkersConfig, |
22 | | |
23 | | #[serde(default)] |
24 | | pub logging: LoggingConfig, |
25 | | |
26 | | #[serde(default)] |
27 | | pub metrics: MetricsConfig, |
28 | | |
29 | | #[serde(default)] |
30 | | pub admin: AdminConfig, |
31 | | |
32 | | #[serde(default)] |
33 | | pub acl: Vec<AclEntry>, |
34 | | |
35 | | #[serde(default)] |
36 | | pub listener: Vec<ListenerConfig>, |
37 | | |
38 | | #[serde(default)] |
39 | | pub doh: DohConfig, |
40 | | |
41 | | #[serde(default)] |
42 | | pub forwarder: Vec<ForwarderConfig>, |
43 | | |
44 | | #[serde(default)] |
45 | | pub root_hints: RootHintsConfig, |
46 | | |
47 | | #[serde(default)] |
48 | | pub cache: CacheConfig, |
49 | | |
50 | | #[serde(default)] |
51 | | pub recursion: RecursionConfig, |
52 | | |
53 | | #[serde(default)] |
54 | | pub ratelimit: RateLimitConfig, |
55 | | |
56 | | #[serde(default)] |
57 | | pub zone: Vec<ZoneConfig>, |
58 | | |
59 | | #[serde(default)] |
60 | | pub tsig_key: Vec<TsigKey>, |
61 | | |
62 | | #[serde(default)] |
63 | | pub axfr: AxfrConfig, |
64 | | |
65 | | #[serde(default)] |
66 | | pub dnssec: DnssecConfig, |
67 | | |
68 | | #[serde(default)] |
69 | | pub policy: PolicyConfig, |
70 | | |
71 | | #[serde(default)] |
72 | | pub amplification_mitigation: AmplificationMitigationConfig, |
73 | | |
74 | | #[serde(default)] |
75 | | pub tuning: TuningConfig, |
76 | | |
77 | | #[serde(default)] |
78 | | pub view: Vec<ViewConfig>, |
79 | | |
80 | | #[serde(default)] |
81 | | pub monitoring: MonitoringConfig, |
82 | | |
83 | | #[serde(default)] |
84 | | pub dynupdate: Vec<DynUpdateConfig>, |
85 | | |
86 | | #[serde(default)] |
87 | | pub limits: LimitsConfig, |
88 | | } |
89 | | |
90 | | impl Config { |
91 | | /// Load config from a JSON file path |
92 | 9 | pub fn from_file(path: &Path) -> Result<Self, SCloudException> { |
93 | 9 | let s = fs::read_to_string(path) |
94 | 9 | .with_context(|| format!0 ("reading config file {}", path0 .display0 ())) |
95 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)?0 ; |
96 | 9 | let cfg: Config = serde_json::from_str(&s) |
97 | 9 | .context("parsing JSON config") |
98 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)?0 ; |
99 | 9 | cfg.validate()?0 ; |
100 | 9 | Ok(cfg) |
101 | 9 | } |
102 | | |
103 | | /// Validation hook |
104 | 10 | pub fn validate(&self) -> Result<(), SCloudException> { |
105 | 24 | let acl_names10 : HashSet<&str>10 = self.acl.iter()10 .map10 (|a| a.name.as_str()).collect10 (); |
106 | 16 | let tsig_names10 : HashSet<&str>10 = self.tsig_key.iter()10 .map10 (|t| t.name.as_str()).collect10 (); |
107 | 10 | let _forwarder_names: HashSet<&str> = |
108 | 24 | self.forwarder.iter()10 .map10 (|f| f.name.as_str()).collect10 (); |
109 | | |
110 | 72 | let is_acl_ref_valid10 = |s: &str| -> bool { |
111 | 72 | if s.trim().is_empty() { |
112 | 0 | return false; |
113 | 72 | } |
114 | 72 | acl_names.contains(s) || s16 .contains16 ('/') |
115 | 72 | }; |
116 | | |
117 | 10 | if self.server.bind_port == 0 { |
118 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT); |
119 | 10 | } |
120 | 10 | if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 { |
121 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD); |
122 | 10 | } |
123 | 10 | if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 { |
124 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
125 | 10 | } |
126 | 10 | if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 { |
127 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
128 | 10 | } |
129 | 10 | if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 { |
130 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
131 | 10 | } |
132 | | |
133 | 10 | let mut listener_names = HashSet::new(); |
134 | 24 | for l in &self.listener10 { |
135 | 24 | if l.name.trim().is_empty() { |
136 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER); |
137 | 24 | } |
138 | 24 | if !listener_names.insert(l.name.as_str()) { |
139 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME); |
140 | 24 | } |
141 | 24 | if l.port == 0 { |
142 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT); |
143 | 24 | } |
144 | 24 | if l.protocols.is_empty() { |
145 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS); |
146 | 24 | } |
147 | 24 | if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) { |
148 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
149 | 24 | } |
150 | | |
151 | 24 | if l.enable_tls.unwrap_or(false) { |
152 | 8 | if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() { |
153 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
154 | 8 | } |
155 | 8 | if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() { |
156 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
157 | 8 | } |
158 | 8 | if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) { |
159 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP); |
160 | 8 | } |
161 | 16 | } |
162 | | } |
163 | | |
164 | 10 | if self.doh.enabled { |
165 | 8 | if self.doh.paths.is_empty() { |
166 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH); |
167 | 8 | } |
168 | 8 | if self.doh.terminate_tls { |
169 | 0 | if self |
170 | 0 | .doh |
171 | 0 | .tls_cert_path |
172 | 0 | .as_deref() |
173 | 0 | .unwrap_or("") |
174 | 0 | .trim() |
175 | 0 | .is_empty() |
176 | | { |
177 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
178 | 0 | } |
179 | 0 | if self |
180 | 0 | .doh |
181 | 0 | .tls_key_path |
182 | 0 | .as_deref() |
183 | 0 | .unwrap_or("") |
184 | 0 | .trim() |
185 | 0 | .is_empty() |
186 | | { |
187 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
188 | 0 | } |
189 | 8 | } |
190 | 2 | } |
191 | | |
192 | 10 | if self.recursion.enabled { |
193 | 8 | if self.recursion.allowed_acl.trim().is_empty() { |
194 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
195 | 8 | } |
196 | 8 | if !is_acl_ref_valid(&self.recursion.allowed_acl) { |
197 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
198 | 8 | } |
199 | 2 | } |
200 | | |
201 | 10 | let mut fwd_names = HashSet::new(); |
202 | 24 | for f in &self.forwarder10 { |
203 | 24 | if f.name.trim().is_empty() { |
204 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
205 | 24 | } |
206 | 24 | if !fwd_names.insert(f.name.as_str()) { |
207 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME); |
208 | 24 | } |
209 | 24 | if f.addresses.is_empty() { |
210 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
211 | 24 | } |
212 | 40 | for a in &f.addresses24 { |
213 | 40 | if a.parse::<std::net::SocketAddr>().is_err() { |
214 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
215 | 40 | } |
216 | | } |
217 | | } |
218 | | |
219 | 10 | let mut zone_names = HashSet::new(); |
220 | 32 | for z in &self.zone10 { |
221 | 32 | if z.name.trim().is_empty() { |
222 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE); |
223 | 32 | } |
224 | 32 | if !zone_names.insert(z.name.as_str()) { |
225 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME); |
226 | 32 | } |
227 | | |
228 | 32 | match z.kind { |
229 | | ZoneType::Master => { |
230 | 16 | let inline = z.inline.unwrap_or(false); |
231 | 16 | if inline { |
232 | 8 | if z.records.is_empty() { |
233 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
234 | 8 | } |
235 | 8 | let has_soa = z |
236 | 8 | .records |
237 | 8 | .iter() |
238 | 8 | .any(|r| r.r#type.eq_ignore_ascii_case("SOA")); |
239 | 8 | if !has_soa { |
240 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
241 | 8 | } |
242 | | } else { |
243 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
244 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
245 | 8 | } |
246 | | } |
247 | | |
248 | 16 | if let Some(acl8 ) = z.notify_acl.as_deref() { |
249 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
250 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
251 | 8 | } |
252 | 8 | } |
253 | 16 | if let Some(acl8 ) = z.allow_transfer_acl.as_deref() { |
254 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
255 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
256 | 8 | } |
257 | 8 | } |
258 | | |
259 | 16 | if let Some(k8 ) = z.axfr_tsig_key.as_deref() { |
260 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
261 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
262 | 8 | } |
263 | 8 | } |
264 | | } |
265 | | ZoneType::Slave => { |
266 | 8 | if z.masters.is_empty() { |
267 | 0 | return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS); |
268 | 8 | } |
269 | 8 | for m in &z.masters { |
270 | 8 | if m.parse::<std::net::SocketAddr>().is_err() { |
271 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
272 | 8 | } |
273 | | } |
274 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
275 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
276 | 8 | } |
277 | | } |
278 | | ZoneType::Forward => { |
279 | 8 | if z.forwarders.is_empty() { |
280 | 0 | return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS); |
281 | 8 | } |
282 | 8 | for f in &z.forwarders { |
283 | 8 | if f.parse::<std::net::SocketAddr>().is_err() { |
284 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
285 | 8 | } |
286 | | } |
287 | | } |
288 | 0 | ZoneType::Stub => { |
289 | 0 | // TODO: not defined JSON yet, strict checks later when I will implement it. |
290 | 0 | } |
291 | | } |
292 | | |
293 | 48 | for r in &z.records32 { |
294 | 48 | if r.r#type.eq_ignore_ascii_case("MX") { |
295 | 8 | if r.priority.is_none() { |
296 | 0 | return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY); |
297 | 8 | } |
298 | 40 | } else if r.priority.is_some() { |
299 | 0 | return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX); |
300 | 40 | } |
301 | | } |
302 | | } |
303 | | |
304 | 10 | let mut view_names = HashSet::new(); |
305 | 16 | for v in &self.view10 { |
306 | 16 | if v.name.trim().is_empty() { |
307 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
308 | 16 | } |
309 | 16 | if !view_names.insert(v.name.as_str()) { |
310 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME); |
311 | 16 | } |
312 | 16 | if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) { |
313 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
314 | 16 | } |
315 | 16 | for vz in &v.zones { |
316 | 16 | if vz.name.trim().is_empty() || vz.file.trim().is_empty() { |
317 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
318 | 16 | } |
319 | | } |
320 | | } |
321 | | |
322 | 10 | for d8 in &self.dynupdate { |
323 | 8 | if d.zone.trim().is_empty() { |
324 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE); |
325 | 8 | } |
326 | 8 | if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) { |
327 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
328 | 8 | } |
329 | 8 | if let Some(k) = d.tsig_key.as_deref() { |
330 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
331 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
332 | 8 | } |
333 | 0 | } |
334 | | |
335 | 8 | if !zone_names.contains(d.zone.as_str()) { |
336 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE); |
337 | 8 | } |
338 | | } |
339 | | |
340 | 10 | Ok(()) |
341 | 10 | } |
342 | | |
343 | | /// Get the address of a specific forwarder by index value |
344 | | #[allow(unused)] |
345 | 5 | pub(crate) fn try_get_forwarder_addr_by_index( |
346 | 5 | &self, |
347 | 5 | forwarder_index: usize, |
348 | 5 | address_index: usize, |
349 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
350 | 5 | let addr = self |
351 | 5 | .forwarder |
352 | 5 | .get(forwarder_index) |
353 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 |
354 | | .addresses |
355 | 5 | .get(address_index) |
356 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)?0 |
357 | 5 | .parse() |
358 | 5 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)?0 ; |
359 | | |
360 | 5 | Ok(addr) |
361 | 5 | } |
362 | | |
363 | | // TODO: add a loop to test the next address for each retry |
364 | 5 | pub(crate) fn try_get_forwarder_addr_by_name( |
365 | 5 | &self, |
366 | 5 | forwarder_name: &str, |
367 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
368 | 5 | let forwarder = self |
369 | 5 | .forwarder |
370 | 5 | .iter() |
371 | 12 | .find5 (|f| f.name == forwarder_name) |
372 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 ; |
373 | | |
374 | 5 | for addr_str in &forwarder.addresses { |
375 | 5 | if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() { |
376 | 5 | return Ok(addr); |
377 | 0 | } |
378 | | } |
379 | | |
380 | 0 | Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR) |
381 | 5 | } |
382 | | } |
383 | | |
384 | | impl Default for Config { |
385 | 5 | fn default() -> Self { |
386 | 5 | Self { |
387 | 5 | server: ServerConfig::default(), |
388 | 5 | workers: WorkersConfig::default(), |
389 | 5 | logging: LoggingConfig::default(), |
390 | 5 | metrics: MetricsConfig::default(), |
391 | 5 | admin: AdminConfig::default(), |
392 | 5 | acl: Vec::new(), |
393 | 5 | listener: Vec::new(), |
394 | 5 | doh: DohConfig::default(), |
395 | 5 | forwarder: Vec::new(), |
396 | 5 | root_hints: RootHintsConfig::default(), |
397 | 5 | cache: CacheConfig::default(), |
398 | 5 | recursion: RecursionConfig::default(), |
399 | 5 | ratelimit: RateLimitConfig::default(), |
400 | 5 | zone: Vec::new(), |
401 | 5 | tsig_key: Vec::new(), |
402 | 5 | axfr: AxfrConfig::default(), |
403 | 5 | dnssec: DnssecConfig::default(), |
404 | 5 | policy: PolicyConfig::default(), |
405 | 5 | amplification_mitigation: AmplificationMitigationConfig::default(), |
406 | 5 | tuning: TuningConfig::default(), |
407 | 5 | view: Vec::new(), |
408 | 5 | monitoring: MonitoringConfig::default(), |
409 | 5 | dynupdate: Vec::new(), |
410 | 5 | limits: LimitsConfig::default(), |
411 | 5 | } |
412 | 5 | } |
413 | | } |
414 | | |
415 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
416 | | pub struct ServerConfig { |
417 | | pub name: String, |
418 | | pub version: String, |
419 | | pub environment: String, |
420 | | pub max_concurrent_requests: usize, |
421 | | pub graceful_shutdown_timeout_secs: u64, |
422 | | |
423 | | pub default_ttl: u32, |
424 | | pub max_udp_payload: usize, |
425 | | pub enable_edns: bool, |
426 | | pub enable_tcp: bool, |
427 | | pub enable_dnssec: bool, |
428 | | |
429 | | pub bind_port: u16, |
430 | | } |
431 | | |
432 | | impl Default for ServerConfig { |
433 | 6 | fn default() -> Self { |
434 | 6 | ServerConfig { |
435 | 6 | name: "scloud-dns".to_string(), |
436 | 6 | version: "none".to_string(), |
437 | 6 | environment: "production".to_string(), |
438 | 6 | max_concurrent_requests: 5000, |
439 | 6 | graceful_shutdown_timeout_secs: 15, |
440 | 6 | default_ttl: 3600, |
441 | 6 | max_udp_payload: 4096, |
442 | 6 | enable_edns: true, |
443 | 6 | enable_tcp: true, |
444 | 6 | enable_dnssec: false, |
445 | 6 | bind_port: 53, |
446 | 6 | } |
447 | 6 | } |
448 | | } |
449 | | |
450 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
451 | | pub struct WorkersConfig { |
452 | | pub tcp_acceptor: u16, |
453 | | #[serde(default)] |
454 | | pub doh_acceptor: u16, |
455 | | pub decoder: u16, |
456 | | pub query_dispatcher: u16, |
457 | | pub cache_lookup: u16, |
458 | | pub zone_manager: u16, |
459 | | pub resolver: u16, |
460 | | pub cache_writer: u16, |
461 | | pub encoder: u16, |
462 | | pub sender: u16, |
463 | | pub cache_janitor: u16, |
464 | | pub metrics: u16, |
465 | | } |
466 | | |
467 | | impl Default for WorkersConfig { |
468 | 5 | fn default() -> Self { |
469 | 5 | WorkersConfig { |
470 | 5 | tcp_acceptor: 1, |
471 | 5 | doh_acceptor: 1, |
472 | 5 | decoder: 5, |
473 | 5 | query_dispatcher: 3, |
474 | 5 | cache_lookup: 3, |
475 | 5 | zone_manager: 1, |
476 | 5 | resolver: 5, |
477 | 5 | cache_writer: 1, |
478 | 5 | encoder: 5, |
479 | 5 | sender: 5, |
480 | 5 | cache_janitor: 1, |
481 | 5 | metrics: 2, |
482 | 5 | } |
483 | 5 | } |
484 | | } |
485 | | |
486 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
487 | | pub struct LoggingConfig { |
488 | | pub level: LogLevel, |
489 | | pub format: LogFormat, |
490 | | pub file: String, |
491 | | pub dyn_ui: bool, |
492 | | pub rotate: bool, |
493 | | pub live_print: bool, |
494 | | pub max_size_mb: u64, |
495 | | } |
496 | | |
497 | | impl Default for LoggingConfig { |
498 | 5 | fn default() -> Self { |
499 | 5 | LoggingConfig { |
500 | 5 | level: LogLevel::INFO, |
501 | 5 | format: LogFormat::TEXT, |
502 | 5 | file: "/var/log/scloud-dns/scloud-dns.log".to_string(), |
503 | 5 | dyn_ui: false, |
504 | 5 | rotate: true, |
505 | 5 | live_print: false, |
506 | 5 | max_size_mb: 200, |
507 | 5 | } |
508 | 5 | } |
509 | | } |
510 | | |
511 | | #[allow(non_camel_case_types)] |
512 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] |
513 | | #[serde(rename_all = "lowercase")] |
514 | | pub enum LogLevel { |
515 | | TRACE = 0, |
516 | | DEBUG = 1, |
517 | | INFO = 2, |
518 | | WARN = 3, |
519 | | ERROR = 4, |
520 | | FATAL = 5, |
521 | | } |
522 | | |
523 | | impl LogLevel { |
524 | 0 | pub fn parse(s: &str) -> Self { |
525 | 0 | match s.to_ascii_lowercase().as_str() { |
526 | 0 | "trace" => Self::TRACE, |
527 | 0 | "debug" => Self::DEBUG, |
528 | 0 | "info" => Self::INFO, |
529 | 0 | "warn" | "warning" => Self::WARN, |
530 | 0 | "error" => Self::ERROR, |
531 | 0 | "fatal" => Self::FATAL, |
532 | 0 | _ => Self::WARN, |
533 | | } |
534 | 0 | } |
535 | | |
536 | 25 | pub(crate) fn as_str(self) -> &'static str { |
537 | 25 | match self { |
538 | 1 | Self::TRACE => "trace", |
539 | 19 | Self::DEBUG => "debug", |
540 | 4 | Self::INFO => "info", |
541 | 0 | Self::WARN => "warn", |
542 | 1 | Self::ERROR => "error", |
543 | 0 | Self::FATAL => "fatal", |
544 | | } |
545 | 25 | } |
546 | | } |
547 | | |
548 | | #[allow(non_camel_case_types)] |
549 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] |
550 | | #[serde(rename_all = "lowercase")] |
551 | | pub enum LogFormat { |
552 | | JSON, |
553 | | TEXT, |
554 | | } |
555 | | |
556 | | impl LogFormat { |
557 | 0 | pub fn parse(s: &str) -> Self { |
558 | 0 | match s.to_ascii_lowercase().as_str() { |
559 | 0 | "json" => Self::JSON, |
560 | 0 | _ => Self::TEXT, |
561 | | } |
562 | 0 | } |
563 | | } |
564 | | |
565 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
566 | | pub struct MetricsConfig { |
567 | | pub enabled: bool, |
568 | | pub prometheus_bind: String, |
569 | | pub enable_health_endpoint: bool, |
570 | | pub health_bind: String, |
571 | | } |
572 | | |
573 | | impl Default for MetricsConfig { |
574 | 5 | fn default() -> Self { |
575 | 5 | MetricsConfig { |
576 | 5 | enabled: true, |
577 | 5 | prometheus_bind: "0.0.0.0:9153".to_string(), |
578 | 5 | enable_health_endpoint: true, |
579 | 5 | health_bind: "127.0.0.1:8081".to_string(), |
580 | 5 | } |
581 | 5 | } |
582 | | } |
583 | | |
584 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
585 | | pub struct AdminConfig { |
586 | | pub enabled: bool, |
587 | | pub bind: String, |
588 | | pub auth_token: String, |
589 | | pub enable_tls: bool, |
590 | | } |
591 | | |
592 | | impl Default for AdminConfig { |
593 | 5 | fn default() -> Self { |
594 | 5 | AdminConfig { |
595 | 5 | enabled: true, |
596 | 5 | bind: "127.0.0.1:8053".to_string(), |
597 | 5 | auth_token: "replace-with-secure-token".to_string(), |
598 | 5 | enable_tls: false, |
599 | 5 | } |
600 | 5 | } |
601 | | } |
602 | | |
603 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
604 | | pub struct AclEntry { |
605 | | pub name: String, |
606 | | pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar |
607 | | } |
608 | | |
609 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
610 | | pub struct ListenerConfig { |
611 | | pub name: String, |
612 | | pub address: String, |
613 | | pub port: u16, |
614 | | #[serde(default)] |
615 | | pub protocols: Vec<Protocol>, |
616 | | #[serde(default)] |
617 | | pub recursion_allowed: bool, |
618 | | /// ACL name or a raw CIDR/list string |
619 | | #[serde(default)] |
620 | | pub acl: String, |
621 | | #[serde(default)] |
622 | | pub workers: Option<usize>, |
623 | | #[serde(default)] |
624 | | pub enable_tls: Option<bool>, |
625 | | #[serde(default)] |
626 | | pub tls_cert_path: Option<String>, |
627 | | #[serde(default)] |
628 | | pub tls_key_path: Option<String>, |
629 | | } |
630 | | |
631 | | impl Default for ListenerConfig { |
632 | 1 | fn default() -> Self { |
633 | 1 | ListenerConfig { |
634 | 1 | name: String::new(), |
635 | 1 | address: "0.0.0.0".to_string(), |
636 | 1 | port: 53, |
637 | 1 | protocols: vec![Protocol::UDP], |
638 | 1 | recursion_allowed: false, |
639 | 1 | acl: "0.0.0.0/0".to_string(), |
640 | 1 | workers: None, |
641 | 1 | enable_tls: None, |
642 | 1 | tls_cert_path: None, |
643 | 1 | tls_key_path: None, |
644 | 1 | } |
645 | 1 | } |
646 | | } |
647 | | |
648 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
649 | | #[serde(rename_all = "lowercase")] |
650 | | pub enum Protocol { |
651 | | UDP, |
652 | | TCP, |
653 | | } |
654 | | |
655 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
656 | | pub struct DohConfig { |
657 | | pub enabled: bool, |
658 | | pub bind: String, |
659 | | #[serde(default)] |
660 | | pub terminate_tls: bool, |
661 | | #[serde(default)] |
662 | | pub tls_cert_path: Option<String>, |
663 | | #[serde(default)] |
664 | | pub tls_key_path: Option<String>, |
665 | | #[serde(default)] |
666 | | pub paths: Vec<String>, |
667 | | #[serde(default)] |
668 | | pub allowed_origins: Vec<String>, |
669 | | } |
670 | | |
671 | | impl Default for DohConfig { |
672 | 6 | fn default() -> Self { |
673 | 6 | DohConfig { |
674 | 6 | enabled: false, |
675 | 6 | bind: "0.0.0.0:8053".to_string(), |
676 | 6 | terminate_tls: false, |
677 | 6 | tls_cert_path: None, |
678 | 6 | tls_key_path: None, |
679 | 6 | paths: vec!["/dns-query".to_string()], |
680 | 6 | allowed_origins: Vec::new(), |
681 | 6 | } |
682 | 6 | } |
683 | | } |
684 | | |
685 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
686 | | pub struct ForwarderConfig { |
687 | | pub name: String, |
688 | | pub addresses: Vec<String>, |
689 | | pub policy: ForwardPolicy, |
690 | | pub timeout_ms: u64, |
691 | | pub edns: bool, |
692 | | pub use_tcp_on_retry: Option<bool>, |
693 | | } |
694 | | |
695 | | impl Default for ForwarderConfig { |
696 | 1 | fn default() -> Self { |
697 | 1 | ForwarderConfig { |
698 | 1 | name: String::new(), |
699 | 1 | addresses: Vec::new(), |
700 | 1 | policy: ForwardPolicy::First, |
701 | 1 | timeout_ms: 1500, |
702 | 1 | edns: true, |
703 | 1 | use_tcp_on_retry: Some(true), |
704 | 1 | } |
705 | 1 | } |
706 | | } |
707 | | |
708 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
709 | | #[serde(rename_all = "snake_case")] |
710 | | #[derive(PartialEq)] |
711 | | pub enum ForwardPolicy { |
712 | | RoundRobin, |
713 | | First, |
714 | | Random, |
715 | | } |
716 | | |
717 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
718 | | pub struct RootHintsConfig { |
719 | | pub file: String, |
720 | | } |
721 | | |
722 | | impl Default for RootHintsConfig { |
723 | 5 | fn default() -> Self { |
724 | 5 | RootHintsConfig { |
725 | 5 | file: "/etc/scloud/root.hints".to_string(), |
726 | 5 | } |
727 | 5 | } |
728 | | } |
729 | | |
730 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
731 | | pub struct CacheConfig { |
732 | | pub enabled: bool, |
733 | | pub max_entries: usize, |
734 | | pub max_ttl_seconds: u64, |
735 | | pub negative_ttl_seconds: u64, |
736 | | pub eviction_policy: String, |
737 | | } |
738 | | |
739 | | impl Default for CacheConfig { |
740 | 6 | fn default() -> Self { |
741 | 6 | CacheConfig { |
742 | 6 | enabled: true, |
743 | 6 | max_entries: 200_000, |
744 | 6 | max_ttl_seconds: 86_400, |
745 | 6 | negative_ttl_seconds: 300, |
746 | 6 | eviction_policy: "lru".to_string(), |
747 | 6 | } |
748 | 6 | } |
749 | | } |
750 | | |
751 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
752 | | pub struct RecursionConfig { |
753 | | pub enabled: bool, |
754 | | pub allowed_acl: String, |
755 | | pub max_recursive_queries: usize, |
756 | | pub recursion_timeout_ms: u64, |
757 | | pub retry_interval_ms: u64, |
758 | | } |
759 | | |
760 | | impl Default for RecursionConfig { |
761 | 6 | fn default() -> Self { |
762 | 6 | RecursionConfig { |
763 | 6 | enabled: false, |
764 | 6 | allowed_acl: "internal".to_string(), |
765 | 6 | max_recursive_queries: 50, |
766 | 6 | recursion_timeout_ms: 5000, |
767 | 6 | retry_interval_ms: 200, |
768 | 6 | } |
769 | 6 | } |
770 | | } |
771 | | |
772 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
773 | | pub struct RateLimitConfig { |
774 | | pub enabled: bool, |
775 | | pub global_qps: u64, |
776 | | pub per_ip_qps: u64, |
777 | | pub per_subnet_qps: u64, |
778 | | pub rrl: RrlConfig, |
779 | | } |
780 | | |
781 | | impl Default for RateLimitConfig { |
782 | 6 | fn default() -> Self { |
783 | 6 | RateLimitConfig { |
784 | 6 | enabled: true, |
785 | 6 | global_qps: 3000, |
786 | 6 | per_ip_qps: 100, |
787 | 6 | per_subnet_qps: 1000, |
788 | 6 | rrl: RrlConfig::default(), |
789 | 6 | } |
790 | 6 | } |
791 | | } |
792 | | |
793 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
794 | | pub struct RrlConfig { |
795 | | pub enabled: bool, |
796 | | pub window_seconds: u64, |
797 | | pub slip: u32, |
798 | | pub qps_threshold: u64, |
799 | | } |
800 | | |
801 | | impl Default for RrlConfig { |
802 | 6 | fn default() -> Self { |
803 | 6 | RrlConfig { |
804 | 6 | enabled: true, |
805 | 6 | window_seconds: 5, |
806 | 6 | slip: 2, |
807 | 6 | qps_threshold: 50, |
808 | 6 | } |
809 | 6 | } |
810 | | } |
811 | | |
812 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
813 | | pub struct ZoneConfig { |
814 | | pub name: String, |
815 | | #[serde(rename = "type")] |
816 | | pub kind: ZoneType, |
817 | | #[serde(default)] |
818 | | pub file: Option<String>, |
819 | | #[serde(default)] |
820 | | pub notify: Option<bool>, |
821 | | #[serde(default)] |
822 | | pub notify_acl: Option<String>, |
823 | | #[serde(default)] |
824 | | pub allow_transfer_acl: Option<String>, |
825 | | #[serde(default)] |
826 | | pub allow_update_acl: Option<String>, |
827 | | #[serde(default)] |
828 | | pub axfr_tsig_key: Option<String>, |
829 | | |
830 | | // Slave-specific |
831 | | #[serde(default)] |
832 | | pub masters: Vec<String>, |
833 | | |
834 | | // Inline zone |
835 | | #[serde(default)] |
836 | | pub inline: Option<bool>, |
837 | | #[serde(default)] |
838 | | pub records: Vec<ZoneRecord>, |
839 | | |
840 | | // Forward-specific |
841 | | #[serde(default)] |
842 | | pub forwarders: Vec<String>, |
843 | | #[serde(default)] |
844 | | pub forward_policy: Option<String>, |
845 | | } |
846 | | |
847 | | impl Default for ZoneConfig { |
848 | 1 | fn default() -> Self { |
849 | 1 | ZoneConfig { |
850 | 1 | name: String::new(), |
851 | 1 | kind: ZoneType::Master, |
852 | 1 | file: None, |
853 | 1 | notify: Some(false), |
854 | 1 | notify_acl: None, |
855 | 1 | allow_transfer_acl: None, |
856 | 1 | allow_update_acl: None, |
857 | 1 | axfr_tsig_key: None, |
858 | 1 | masters: Vec::new(), |
859 | 1 | inline: Some(false), |
860 | 1 | records: Vec::new(), |
861 | 1 | forwarders: Vec::new(), |
862 | 1 | forward_policy: None, |
863 | 1 | } |
864 | 1 | } |
865 | | } |
866 | | |
867 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
868 | | #[serde(rename_all = "lowercase")] |
869 | | #[derive(PartialEq)] |
870 | | pub enum ZoneType { |
871 | | Master, |
872 | | Slave, |
873 | | Forward, |
874 | | Stub, |
875 | | } |
876 | | |
877 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
878 | | pub struct ZoneRecord { |
879 | | pub name: String, |
880 | | pub ttl: Option<u32>, |
881 | | pub class: Option<String>, |
882 | | #[serde(rename = "type")] |
883 | | pub r#type: String, |
884 | | pub rdata: String, |
885 | | #[serde(default)] |
886 | | pub priority: Option<u16>, |
887 | | } |
888 | | |
889 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
890 | | pub struct TsigKey { |
891 | | pub name: String, |
892 | | pub algorithm: String, |
893 | | pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production |
894 | | } |
895 | | |
896 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
897 | | pub struct AxfrConfig { |
898 | | pub enabled: bool, |
899 | | pub max_concurrent_transfers: usize, |
900 | | pub transfer_timeout_secs: u64, |
901 | | } |
902 | | |
903 | | impl Default for AxfrConfig { |
904 | 6 | fn default() -> Self { |
905 | 6 | AxfrConfig { |
906 | 6 | enabled: true, |
907 | 6 | max_concurrent_transfers: 4, |
908 | 6 | transfer_timeout_secs: 120, |
909 | 6 | } |
910 | 6 | } |
911 | | } |
912 | | |
913 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
914 | | pub struct DnssecConfig { |
915 | | pub enabled: bool, |
916 | | pub auto_sign: bool, |
917 | | pub default_algo: String, |
918 | | pub kasp_file: Option<String>, |
919 | | } |
920 | | |
921 | | impl Default for DnssecConfig { |
922 | 6 | fn default() -> Self { |
923 | 6 | DnssecConfig { |
924 | 6 | enabled: false, |
925 | 6 | auto_sign: false, |
926 | 6 | default_algo: "RSASHA256".to_string(), |
927 | 6 | kasp_file: None, |
928 | 6 | } |
929 | 6 | } |
930 | | } |
931 | | |
932 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
933 | | pub struct PolicyConfig { |
934 | | #[serde(default)] |
935 | | pub deny_domains: Vec<String>, |
936 | | } |
937 | | |
938 | | impl Default for PolicyConfig { |
939 | 5 | fn default() -> Self { |
940 | 5 | PolicyConfig { |
941 | 5 | deny_domains: Vec::new(), |
942 | 5 | } |
943 | 5 | } |
944 | | } |
945 | | |
946 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
947 | | pub struct AmplificationMitigationConfig { |
948 | | pub drop_fragments: bool, |
949 | | pub max_response_size_udp: usize, |
950 | | } |
951 | | |
952 | | impl Default for AmplificationMitigationConfig { |
953 | 5 | fn default() -> Self { |
954 | 5 | AmplificationMitigationConfig { |
955 | 5 | drop_fragments: true, |
956 | 5 | max_response_size_udp: 4096, |
957 | 5 | } |
958 | 5 | } |
959 | | } |
960 | | |
961 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
962 | | pub struct TuningConfig { |
963 | | pub socket_recv_buffer_bytes: usize, |
964 | | pub socket_send_buffer_bytes: usize, |
965 | | pub max_label_length: usize, |
966 | | pub max_domain_length: usize, |
967 | | } |
968 | | |
969 | | impl Default for TuningConfig { |
970 | 5 | fn default() -> Self { |
971 | 5 | TuningConfig { |
972 | 5 | socket_recv_buffer_bytes: 262_144, |
973 | 5 | socket_send_buffer_bytes: 262_144, |
974 | 5 | max_label_length: 63, |
975 | 5 | max_domain_length: 253, |
976 | 5 | } |
977 | 5 | } |
978 | | } |
979 | | |
980 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
981 | | pub struct ViewConfig { |
982 | | pub name: String, |
983 | | pub acl: String, |
984 | | #[serde(default)] |
985 | | pub zones: Vec<ViewZone>, |
986 | | } |
987 | | |
988 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
989 | | pub struct ViewZone { |
990 | | pub name: String, |
991 | | pub file: String, |
992 | | } |
993 | | |
994 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
995 | | pub struct MonitoringConfig { |
996 | | pub enable_query_logging: bool, |
997 | | pub query_log_path: String, |
998 | | pub log_query_qps: u64, |
999 | | } |
1000 | | |
1001 | | impl Default for MonitoringConfig { |
1002 | 5 | fn default() -> Self { |
1003 | 5 | MonitoringConfig { |
1004 | 5 | enable_query_logging: false, |
1005 | 5 | query_log_path: "/var/log/scloud-dns/queries.log".to_string(), |
1006 | 5 | log_query_qps: 1000, |
1007 | 5 | } |
1008 | 5 | } |
1009 | | } |
1010 | | |
1011 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1012 | | pub struct DynUpdateConfig { |
1013 | | pub zone: String, |
1014 | | pub acl: String, |
1015 | | pub tsig_key: Option<String>, |
1016 | | pub allow: bool, |
1017 | | } |
1018 | | |
1019 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1020 | | pub struct LimitsConfig { |
1021 | | pub max_udp_packet_size: usize, |
1022 | | pub max_queries_per_minute_per_ip: u64, |
1023 | | pub max_tcp_sessions_per_ip: usize, |
1024 | | } |
1025 | | |
1026 | | impl Default for LimitsConfig { |
1027 | 6 | fn default() -> Self { |
1028 | 6 | LimitsConfig { |
1029 | 6 | max_udp_packet_size: 4096, |
1030 | 6 | max_queries_per_minute_per_ip: 1000, |
1031 | 6 | max_tcp_sessions_per_ip: 8, |
1032 | 6 | } |
1033 | 6 | } |
1034 | | } |